// standard lib
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>

// linux socket
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/socket.h>

// head 
#include "UDPServer.h"

void UDPServer::Error(const char* message)
{
    cout << message << strerror(errno) << "(errno: " << errno << ")" << endl;
	close(server_fd);
	exit(-1);
}

int64 UDPServer::GetTickCount()
{
    timespec now;
    int64 sec, nsec;
 
    clock_gettime(CLOCK_MONOTONIC, &now);
    sec = now.tv_sec;
    nsec = now.tv_nsec;
 
    return sec * 1000 + nsec / 1000000;
}

void UDPServer::InitUDPServer()
{
    /* init udp server socket */
	
	// @PF_INET: Protocol Family
	// @SOCK_DGRAM: UDP/IP
	if ((server_fd = socket(PF_INET, SOCK_DGRAM, 0)) == -1)
	{
        cout << "Server create udp socket error" << strerror(errno)
            << "(errno: )" << errno << ")"
            << endl;
		exit(-1);
	}

    // set content in serveraddr struct to 0
	bzero(&udp_server_addr, sizeof(udp_server_addr));

	udp_server_addr.sin_family = AF_INET; // @AF_INET: 
	udp_server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // receive packets from any ip
	udp_server_addr.sin_port = htons(UDP_SERVER_PORT); // bind port

    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    if ((bind(server_fd, (struct sockaddr*)&udp_server_addr, sizeof(sockaddr)) == -1))
	{
		Error("UDP Server bind socket error: ");
	}

    // display the host bound
    char serverHost[256];
    gethostname(serverHost, 256);
    hostent* pHost = gethostbyname(serverHost);
    in_addr addr;
    for (int i = 0; ; i++)
    {
        char *p = pHost->h_addr_list[i];
        if (p == nullptr) break;

        memcpy(&addr.s_addr, p, pHost->h_length);
        printf("bind to local address -> %s:%d \n", inet_ntoa(addr), UDP_SERVER_PORT);
    }

    printf("start P2P server....\n");

    pthread_mutex_init(&peerListLock, NULL);
    pthread_create(&handleClientRequestTid, NULL, HandleClientRequest, (void*)this);
}

void* UDPServer::HandleClientRequest(void* udpserver)
{
    UDPServer* that = (UDPServer*)udpserver;
    char buff[MAX_PACKET_SIZE];
    Message* msg = (Message*)buff;
    sockaddr_in clientAddr;
    socklen_t addr_len = sizeof(clientAddr);
    int recv_len = sizeof(clientAddr);
    while (true) 
    {
        recv_len = recvfrom(that->server_fd, buff, MAX_PACKET_SIZE, 0, (sockaddr*)&clientAddr, &addr_len);
        if (recv_len < 0)
        {
            that->Error("Server recv msg errr: ");
        }
        if (recv_len < sizeof(Message)) { continue; }

        switch(msg->type)
        {
            case USERLOGIN: // user login
                {
                    msg->peer.addr[msg->peer.addrNum].ip = clientAddr.sin_addr.s_addr;
                    msg->peer.addr[msg->peer.addrNum].port = ntohs(clientAddr.sin_port);
                    msg->peer.addrNum ++;
                    msg->peer.LastActiveTime = that->GetTickCount();

                    // save user information into peer list
                    pthread_mutex_lock(&peerListLock);
                    bool success = that->peerList->AddPeer(&msg->peer);
                    pthread_mutex_unlock(&peerListLock);

                    if (success)
                    {
                        msg->type = USERLOGACK;
                        if (sendto(that->server_fd, (char*)msg, sizeof(Message), 0,
                            (sockaddr*)&clientAddr, sizeof(clientAddr)) < 0)
                        {
                            that->Error("server send USERLOGACK error: ");
                        }
                        printf("has a user login: %s (%s:%d)\n",
                            msg->peer.username, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
                    }
                }
                break;

            case USERLOGOUT:
                {
                    pthread_mutex_lock(&peerListLock);
                    that->peerList->DeletePeer(msg->peer.username);
                    pthread_mutex_unlock(&peerListLock);

                    printf("has a user logout: %s(%s:%d) \n",
                        msg->peer.username, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
                    printf("Current peer list size: %d\n", that->peerList->currrentSize);
                }
                break;

            case GETPEERLIST:
                {
                    printf("sending user list information to %s (%s:%d)...\n",
                        msg->peer.username, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
                    Message peerMsg;
                    peerMsg.type = GETPEERLIST;
                    for (int i = 0; i < that->peerList->currrentSize; i++)
                    {
                        memcpy(&peerMsg.peer, &that->peerList->h_peerList[i], sizeof(PEER_INFO));
                        sendto(that->server_fd, (char*)&peerMsg, sizeof(peerMsg), 0,
                            (sockaddr*)&clientAddr, sizeof(clientAddr));
                    }
                    // send complete packet
                    peerMsg.type = USERLISTCMP;
                    sendto(that->server_fd, (char*)&peerMsg, sizeof(peerMsg), 0,
                        (sockaddr*)&clientAddr, sizeof(clientAddr));
                }
                break;
            
            case REQUESTPUNCH:
                {
                    // find username
                    char* user = (char*)(msg + 1);
                    printf("%s wants to connect to %s", msg->peer.username, user);

                    // note the punch requester
                    memcpy(that->request_source_name, msg->peer.username, sizeof(that->request_source_name));

                    pthread_mutex_lock(&peerListLock);
                    PEER_INFO* peerInfo = that->peerList->GetPeer(user);
                    pthread_mutex_unlock(&peerListLock);

                    if (peerInfo != nullptr)
                    {
                        // @addrNum - 1: the last ip:port which is the public one
                        clientAddr.sin_addr.s_addr = peerInfo->addr[peerInfo->addrNum - 1].ip;
                        clientAddr.sin_port = htons(peerInfo->addr[peerInfo->addrNum - 1].port);

                        pthread_mutex_lock(&peerListLock);
                        PEER_INFO* A_info = that->peerList->GetPeer(msg->peer.username);
                        pthread_mutex_unlock(&peerListLock);
                        memcpy(&msg->peer, A_info, sizeof(A_info));
                
                        sendto(that->server_fd, (char*)msg, MAX_PACKET_SIZE, 0,
                            (sockaddr*)&clientAddr, sizeof(clientAddr));
                    }
                }
                break;
            
            case REQUESTPUNCHACK:
                {
                    /* tell A that there is a hole from B to A, A could send msg to B */
                    printf("Server ack...to A: %s\n", that->request_source_name);

                    pthread_mutex_lock(&peerListLock);
                    PEER_INFO* peerInfo = that->peerList->GetPeer(that->request_source_name);
                    pthread_mutex_unlock(&peerListLock);


                    if (peerInfo != nullptr)
                    {
                        // @addrNum - 1: the last ip:port which is the public one
                        clientAddr.sin_addr.s_addr = peerInfo->addr[peerInfo->addrNum - 1].ip;
                        clientAddr.sin_port = htons(peerInfo->addr[peerInfo->addrNum - 1].port);

                        printf("target ip: %s , ", inet_ntoa(clientAddr.sin_addr));
                        printf("port: %d\n", ntohs(clientAddr.sin_port));

                        printf("test b username: %s\n", msg->peer.username);
                        pthread_mutex_lock(&peerListLock);
                        PEER_INFO* B_info = that->peerList->GetPeer(msg->peer.username);
                        pthread_mutex_unlock(&peerListLock);
                        memcpy(&msg->peer.addr, &B_info->addr[B_info->addrNum - 1], sizeof(B_info->addr[B_info->addrNum - 1]));

                        sendto(that->server_fd, (char*)msg, sizeof(Message), 0,
                            (sockaddr*)&clientAddr, sizeof(clientAddr));
                    }
                }
                break;
            
            case USERACTIVEQUERYACK:
                {
                    printf("recv active ack msg from %s (%s:%d) \n",
                        msg->peer.username, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

                    pthread_mutex_lock(&peerListLock);
                    PEER_INFO *peerInfo = that->peerList->GetPeer(msg->peer.username);
                    if (peerInfo != nullptr)
                    {
                        peerInfo->LastActiveTime = that->GetTickCount();
                    }
                    pthread_mutex_unlock(&peerListLock);
                }
                break;
        }
    }
}

